iT邦幫忙

2024 iThome 鐵人賽

DAY 5
0

在 Dart ,Flutter 的程式設計中,控制流程和迴圈同樣是非常基本且重要的概念,Dart 的控制流程語法基本上和其他語言非常類似,當然也存在一些獨特之處。

冷知識:Cupertino 開頭例如 CupertinoButton 為 iOS 風格逐漸組件,Cupertino 是蘋果總部所在的城市。

if/else

if/else 用於根據條件執行不同的程式碼區塊。Dart 的 if 語句可以不使用 {},尤其在 Flutter 的 Widget 定義非常實用。

Widget build(BuildContext context) {
  return Column(
    children: [
      Text('持續顯示'),
      if (condition)
        Text('符合條件顯示'),
      Text('其他'),
    ],
  );
}

如果 if 語句的表達式結果是 true 則會執行後續的表達式

int a = 1;
if (a == 1) {
  // 多個語句
}

if (a == 2) print(2);
  // 不使用 {} 只能接受一個語句

需要注意的是, Dart 跟 JavaScript 不一樣,它沒有 Truthy 和 Falsy 的概念。也就是所有條件式必須明確為布林值 truefalse

String s = 'true';
if (s) { // 錯誤:條件必須是布林值
}

在像是 JavaScript 的語言,兩個變數相等可以使用 =====,前者會嘗試將兩者轉換為相同型別,然後檢查值是否相等,後者會檢查型別和值是否相等。例如在 JavaScript "7" == 7true,但 "7" === 7 則是 false。因為轉型的設計容易造成初階開發者的困惑,因此 Dart 並不會自動為我們轉型。雖然在撰寫時會感覺好像很麻煩,沒那麼方便,但長期對程式的品質而言比較健康。

while 和 do-while 迴圈

whiledo-while 迴圈用於重複執行程式碼,會在條件式為 true 的時候重複某段特定的程式。false 的時候終止。

do-whilewhile 的差異在於是在程式碼結束才判斷條件或是在迴圈的一開始就判斷:

int counter = 0;
while (counter < 2) {
  print(counter);
  counter++;
}
// 0, 1

do {
  print(counter);
  counter++;
} while (counter < 2);
// 2

for 迴圈

for 迴圈提供了一種簡潔的方式來重複執行程式碼。它的基本結構如下:

for (初始化; 條件; 遞增/遞減) {
  // 
}
for (int i = 0; i < 5; i++) {
  print(i);
}

break 和 continue

breakcontinue關鍵字用於控制迴圈的執行流程。使用 break 可以讓我們自己跳出這個迴圈,continue 則是進入下一次迴圈:

int counter = 0;

while (counter < 10) {
  counter++;
  
  if (counter == 4) {
    break;
  } else (counter == 2) {
    continue;
  }
  
  print(counter);
}
// 1, 3

switch

switch 語句可以根據變數的值選擇分支執行程式碼。很類似 if/else 結構,但如果你希望一組特定值在分支執行,並保證每個值都有對應的分支。

這些特定的值,也叫 enum 列舉。switch 傳入一變數,然後為可能的值定義程式碼分支。

String location = ...;
switch (location) {
  case 'Whitby':
    //
    break;
  case 'Saltburn';
    //
    break;
  default:
    //
}

關於 switch 還有一些重點:

  • 如果沒有使用 breakswtich 語句會繼續執行下一 case
  • 如果沒有任何匹配的 case 則執行 defaultdefault 是可選的。

在Dart 3.0中,switch 語句支援了更複雜的模式匹配:

var pair = (1, 'one');
switch (pair) {
  case (int x, String y) when x > 0:
    print('正數: $y');
  case (0, String y):
    print('零: $y');
  default:
    print('其他情況');
}

詳細的教學可以參考官方介紹

函式和方法

函式和方法都是將特定任務包含在其中的程式碼片段,語法上是相同的。

通常函式回傳的型別可以忽略不宣告,因為通常你回傳的值,Dart 都可以解析出型別,如果沒有 return 則假設回傳一個 dynamic 型別,如果是真的不用回傳則宣告 void

void main() {
  String str = getStr();
  print(getStr());
}
String getStr() {
  return "Hi";
}
// 可以省略,回傳型別為 dynamic
getNext() {
  return "Next one";
}

在這個範例中 getStr 為最高層級函式,換句話說,雖然 Dart 是物件導向語言,但不一定要用類別 class 封裝函式。

函式參數

Parameters(參數) v.s Arguments(引數)

簡單說參數可理解為函式定義時列出來的變數佔位符,而引數則是呼叫提供的具體值。

void greet(String msg) { // msg 參數
	print(msg);
}
String content = 'Hey';
greet(content); // content 是引數
greet("Hi"); // Hi 是引數
  • 所謂參數指的是 函式語法格式中的項目 這個項目是用來宣告輸入資料的型別和名稱,就如同上面 msg

  • 而引數就是我們呼叫函式時傳入的資料,當我們呼叫函式時傳入引數的型別必須要符合參數宣告的型別。

一個函式可以有兩種類型的參數;必須和可選。此外,參數還可以具名,而不依賴順序位置,這讓程式碼更具可讀性。在 Flutter,Widget 包含很多可選參數,因此確定那些引數適用於參數對於理解程式碼非常重要。

一個參數的型別不是一定要宣告的;在這種情況下,參數會先假設為 dynamic 型別。但為了程式碼的可讀性和可維護性,建議還是加入型別宣告。

必填位置參數;必須 + 按照順序位置的參數

最簡單的函式定義就是使用按順序參數的方式來宣告。這是大部分語言常見的用法,你大概率已經很熟悉這種宣告方式。參數依照順序排列,而在呼叫函式的時候引數也是一樣按照相同順序如下範例:

sayHappyBirthday(String name, int age) {
  return "$name is ${age.toString()} years old.";
}

// 呼叫
sayHappyBirthday("andyyou", 37);

可選位置參數;按照順序位置的可選參數

有時候並不是全部的參數都一定強制要傳入,因此我們可以宣告可選參數。可選參數使用 [] 來宣告。要注意的是可選參數一定要在必填參數後面:

sayHappyBirthday(String name, [int? age]) {
  
}

如果我們不傳入 age 那麼 age 將會是 null 也因此宣告時需要使用 int?,當然如果你不希望允許為 null 那麼可以設定預設值

sayHappyBirthday(String name, [int age = 18]) {
  
}

讓我們重新來理解這個部分,[] 只決定了呼叫時參數能否被省略。一旦你宣告了,表示函式內部還是會用到這個參數 age,也就是說當省略傳入時,不是 null 就一定是預設值取決於你的設計需求。換句話說也就是;不會有 int age 卻沒有預設值的情況。

具名參數

具名參數使用 {} 語法定義。這些定義須在必填參數之後。跟可選位置參數一樣可以使用預設值。至於必填參數都已經是必填了,設定預設值永遠也無法使用。

sayHappyBirthday(String name, { int age = 7 }) {
  
}

呼叫時,具名參數須指定名稱(這個就是我們在 Flutter 使用 Widget 時會經常遇到的使用方式)

sayHappyBirthday("Laura", age: 21);

預設具名參數是可選的;呼叫時不一定要包含其引數,也就是預設具名參數要嘛是可以為空,要嘛是有預設值。但除此之外還可以使用 required 改成必填。

sayHappyBirthday(String name, { required int age }) {
  
}

函式參數總結:

  • 「必填位置參數」須按照順序傳入引數,範例 greeting(String name, int age)
  • 「可選位置參數」,必須位於「必填參數」後面,呼叫時可省略,省略的話預設為 null 或者預設值。範例 greeting(String name, [int? age])
  • 「具名參數」,使用 {} 語法,預設為可選,可選時呼叫可省略,值一樣為 null 或預設值,還可以使用 required 變更為必填 greeting(String name, { required int age })

Record

Dart 3 之後新增了新的功能 Record。這是新的資料型別,類似集合,讓我們可以在一個物件中存放一物件集合。

它可以混合位置參數和具名參數,Record 的語法類似函式參數:

(type1, type2, {<type3> name})

舉例來說如果你希望宣告一個 Record 型別的變數可以如下

(String, int) variables;

variables = ("andyyou", 20);

具名的用法也很類似函式參數:

({ String place, int distance }) point;
point = (place: 'Taiwan', distance: 200);

Record 的強大之處在於可以讓你支援函式包含多個回傳值,就跟我們可以為函式定義多個參數一樣。

舉例來說,假設你希望一個比賽的賽況資料區分為隊名和其分數,在支援 Record 型別之前,我們通常需要定義一個專用的類別 Class ,有了 Record 之後我們可以如下

(String, int, String, int) getScore() {
  return ("Team 1", 4, "Team 2", 10);
}

然後呼叫函式之後的回傳結果我們就可以用索引來取得

var score = getScore();
var t1 = score.$1;

更進一步,如果我們要讓程式的可讀性更好可以使用解構

var (homeTeam, homeScore, awayTeam, awayScore) = getScore();

函式作為型別

在 Dart 中函式 Function 被視為一種型別就像 Stringnum 型別一樣。表示函式可以被賦值給變數或作為其他函式的參數。

void greet() {
  print("Hello, world!");
}

void main() {
  Function x = greet;
  x();
  
  var y = greet;
  String msg = y();
  print(msg);
}

匿名函式

匿名函式就是沒有名稱的函式;或稱為 Lambda 或閉包。例如 ListforEach() 就是一個很好的例子。我們需要傳入一個函式,列表的每一個元素都會和這個函式搭配執行。

void main() {
  List list = [1, 2, 3, 4];
  list.forEach((number) => print('Hi, $number'));
}

上面範例我們傳入匿名函式

(number) => print('Hi, $number')

=>也就是大家熟悉的箭頭函式,該匿名函式有一個參數,然後使用 print

詞彙作用域/範圍(Scope)

Lexical Scoping 詞彙作用域是一種作用域規則,基於程式碼的結構和層次決定一個變數的作用域。也就是一個變數所在的位置會決定誰能存取或被執行。

globals() {
  print('Top-level');
}

simple() {
  globals() {
    print('This is nested function');
  }
}

main() {
  simple();
  globals();
}

通過理解和運用這些控制流程和函式,我們進一步可以理解開發 Flutter 時遇到的那些奇妙語法。下一個章節,我們將開始探討物件導向的部分。


上一篇
Day 4 Dart 基礎 (上)
下一篇
Day 6 Dart 物件導向 (上)
系列文
Flutter 開發實戰 - 30 天逃離新手村38
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言